import PointerEvent from './PointerEvent';
import PointerMap from './pointermap';

var CLONE_PROPS = [

  // MouseEvent
  'bubbles',
  'cancelable',
  'view',
  'detail',
  'screenX',
  'screenY',
  'clientX',
  'clientY',
  'ctrlKey',
  'altKey',
  'shiftKey',
  'metaKey',
  'button',
  'relatedTarget',

  // DOM Level 3
  'buttons',

  // PointerEvent
  'pointerId',
  'width',
  'height',
  'pressure',
  'tiltX',
  'tiltY',
  'pointerType',
  'hwTimestamp',
  'isPrimary',

  // event instance
  'type',
  'target',
  'currentTarget',
  'which',
  'pageX',
  'pageY',
  'timeStamp'
];

var CLONE_DEFAULTS = [

  // MouseEvent
  false,
  false,
  null,
  null,
  0,
  0,
  0,
  0,
  false,
  false,
  false,
  false,
  0,
  null,

  // DOM Level 3
  0,

  // PointerEvent
  0,
  0,
  0,
  0,
  0,
  0,
  '',
  0,
  false,

  // event instance
  '',
  null,
  null,
  0,
  0,
  0,
  0
];

var BOUNDARY_EVENTS = {
  'pointerover': 1,
  'pointerout': 1,
  'pointerenter': 1,
  'pointerleave': 1
};

var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined');

/**
 * This module is for normalizing events. Mouse and Touch events will be
 * collected here, and fire PointerEvents that have the same semantics, no
 * matter the source.
 * Events fired:
 *   - pointerdown: a pointing is added
 *   - pointerup: a pointer is removed
 *   - pointermove: a pointer is moved
 *   - pointerover: a pointer crosses into an element
 *   - pointerout: a pointer leaves an element
 *   - pointercancel: a pointer will no longer generate events
 */
var dispatcher = {
  pointermap: new PointerMap(),
  eventMap: Object.create(null),
  captureInfo: Object.create(null),

  // Scope objects for native events.
  // This exists for ease of testing.
  eventSources: Object.create(null),
  eventSourceList: [],
  /**
   * Add a new event source that will generate pointer events.
   *
   * `inSource` must contain an array of event names named `events`, and
   * functions with the names specified in the `events` array.
   * @param {string} name A name for the event source
   * @param {Object} source A new source of platform events.
   */
  registerSource: function(name, source) {
    var s = source;
    var newEvents = s.events;
    if (newEvents) {
      newEvents.forEach(function(e) {
        if (s[e]) {
          this.eventMap[e] = s[e].bind(s);
        }
      }, this);
      this.eventSources[name] = s;
      this.eventSourceList.push(s);
    }
  },
  register: function(element) {
    var l = this.eventSourceList.length;
    for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) {

      // call eventsource register
      es.register.call(es, element);
    }
  },
  unregister: function(element) {
    var l = this.eventSourceList.length;
    for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) {

      // call eventsource register
      es.unregister.call(es, element);
    }
  },
  contains: /*scope.external.contains || */function(container, contained) {
    try {
      return container.contains(contained);
    } catch (ex) {

      // most likely: https://bugzilla.mozilla.org/show_bug.cgi?id=208427
      return false;
    }
  },

  // EVENTS
  down: function(inEvent) {
    inEvent.bubbles = true;
    this.fireEvent('pointerdown', inEvent);
  },
  move: function(inEvent) {
    inEvent.bubbles = true;
    this.fireEvent('pointermove', inEvent);
  },
  up: function(inEvent) {
    inEvent.bubbles = true;
    this.fireEvent('pointerup', inEvent);
  },
  enter: function(inEvent) {
    inEvent.bubbles = false;
    this.fireEvent('pointerenter', inEvent);
  },
  leave: function(inEvent) {
    inEvent.bubbles = false;
    this.fireEvent('pointerleave', inEvent);
  },
  over: function(inEvent) {
    inEvent.bubbles = true;
    this.fireEvent('pointerover', inEvent);
  },
  out: function(inEvent) {
    inEvent.bubbles = true;
    this.fireEvent('pointerout', inEvent);
  },
  cancel: function(inEvent) {
    inEvent.bubbles = true;
    this.fireEvent('pointercancel', inEvent);
  },
  leaveOut: function(event) {
    this.out(event);
    this.propagate(event, this.leave, false);
  },
  enterOver: function(event) {
    this.over(event);
    this.propagate(event, this.enter, true);
  },

  // LISTENER LOGIC
  eventHandler: function(inEvent) {

    // This is used to prevent multiple dispatch of pointerevents from
    // platform events. This can happen when two elements in different scopes
    // are set up to create pointer events, which is relevant to Shadow DOM.
    if (inEvent._handledByPE) {
      return;
    }
    var type = inEvent.type;
    var fn = this.eventMap && this.eventMap[type];
    if (fn) {
      fn(inEvent);
    }
    inEvent._handledByPE = true;
  },

  // set up event listeners
  listen: function(target, events) {
    events.forEach(function(e) {
      this.addEvent(target, e);
    }, this);
  },

  // remove event listeners
  unlisten: function(target, events) {
    events.forEach(function(e) {
      this.removeEvent(target, e);
    }, this);
  },
  addEvent: /*scope.external.addEvent || */function(target, eventName) {
    target.addEventListener(eventName, this.boundHandler);
  },
  removeEvent: /*scope.external.removeEvent || */function(target, eventName) {
    target.removeEventListener(eventName, this.boundHandler);
  },

  // EVENT CREATION AND TRACKING
  /**
   * Creates a new Event of type `inType`, based on the information in
   * `inEvent`.
   *
   * @param {string} inType A string representing the type of event to create
   * @param {Event} inEvent A platform event with a target
   * @return {Event} A PointerEvent of type `inType`
   */
  makeEvent: function(inType, inEvent) {

    // relatedTarget must be null if pointer is captured
    if (this.captureInfo[inEvent.pointerId]) {
      inEvent.relatedTarget = null;
    }
    var e = new PointerEvent(inType, inEvent);
    if (inEvent.preventDefault) {
      e.preventDefault = inEvent.preventDefault;
    }
    e._target = e._target || inEvent.target;
    return e;
  },

  // make and dispatch an event in one call
  fireEvent: function(inType, inEvent) {
    var e = this.makeEvent(inType, inEvent);
    return this.dispatchEvent(e);
  },
  /**
   * Returns a snapshot of inEvent, with writable properties.
   *
   * @param {Event} inEvent An event that contains properties to copy.
   * @return {Object} An object containing shallow copies of `inEvent`'s
   *    properties.
   */
  cloneEvent: function(inEvent) {
    var eventCopy = Object.create(null);
    var p;
    for (var i = 0; i < CLONE_PROPS.length; i++) {
      p = CLONE_PROPS[i];
      eventCopy[p] = inEvent[p] || CLONE_DEFAULTS[i];

      // Work around SVGInstanceElement shadow tree
      // Return the <use> element that is represented by the instance for Safari, Chrome, IE.
      // This is the behavior implemented by Firefox.
      if (HAS_SVG_INSTANCE && (p === 'target' || p === 'relatedTarget')) {
        if (eventCopy[p] instanceof SVGElementInstance) {
          eventCopy[p] = eventCopy[p].correspondingUseElement;
        }
      }
    }

    // keep the semantics of preventDefault
    if (inEvent.preventDefault) {
      eventCopy.preventDefault = function() {
        inEvent.preventDefault();
      };
    }
    return eventCopy;
  },
  getTarget: function(inEvent) {
    var capture = this.captureInfo[inEvent.pointerId];
    if (!capture) {
      return inEvent._target;
    }
    if (inEvent._target === capture || !(inEvent.type in BOUNDARY_EVENTS)) {
      return capture;
    }
  },
  propagate: function(event, fn, propagateDown) {
    var target = event.target;
    var targets = [];

    // Order of conditions due to document.contains() missing in IE.
    while (target != null && target !== document && !target.contains(event.relatedTarget)) {
      targets.push(target);
      target = target.parentNode;

      // Touch: Do not propagate if node is detached.
      if (!target) {
        return;
      }
    }
    if (propagateDown) {
      targets.reverse();
    }
    targets.forEach(function(target) {
      event.target = target;
      fn.call(this, event);
    }, this);
  },
  setCapture: function(inPointerId, inTarget, skipDispatch) {
    if (this.captureInfo[inPointerId]) {
      this.releaseCapture(inPointerId, skipDispatch);
    }

    this.captureInfo[inPointerId] = inTarget;
    this.implicitRelease = this.releaseCapture.bind(this, inPointerId, skipDispatch);
    document.addEventListener('pointerup', this.implicitRelease);
    document.addEventListener('pointercancel', this.implicitRelease);

    var e = new PointerEvent('gotpointercapture', { bubbles: true });
    e.pointerId = inPointerId;
    e._target = inTarget;

    if (!skipDispatch) {
      this.asyncDispatchEvent(e);
    }
  },
  releaseCapture: function(inPointerId, skipDispatch) {
    var t = this.captureInfo[inPointerId];
    if (!t) {
      return;
    }

    this.captureInfo[inPointerId] = undefined;
    document.removeEventListener('pointerup', this.implicitRelease);
    document.removeEventListener('pointercancel', this.implicitRelease);

    var e = new PointerEvent('lostpointercapture', { bubbles: true });
    e.pointerId = inPointerId;
    e._target = t;

    if (!skipDispatch) {
      this.asyncDispatchEvent(e);
    }
  },
  /**
   * Dispatches the event to its target.
   *
   * @param {Event} inEvent The event to be dispatched.
   * @return {Boolean} True if an event handler returns true, false otherwise.
   */
  dispatchEvent: /*scope.external.dispatchEvent || */function(inEvent) {
    var t = this.getTarget(inEvent);
    if (t) {
      return t.dispatchEvent(inEvent);
    }
  },
  asyncDispatchEvent: function(inEvent) {
    requestAnimationFrame(this.dispatchEvent.bind(this, inEvent));
  }
};
dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher);

export default dispatcher;
